iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Build on Google AI

LOCAL:30 天打造 LINE × Google AI 地方服務 Agent系列 第 4

Day 4|讓 Gemini 走進 LINE:先驗簽,再回覆,不把收到當完成

  • 分享至 

  • xImage
  •  

前一篇讓 Gemini 說明合成的地方服務狀態,今天把這個回合接到 LINE 測試頻道。用原始請求內容驗簽,再檢查使用者、事件與指令,避免任何人都能觸發模型呼叫。本文拆開 Webhook 收件、模型產生文字、LINE 接受回覆,以及手機實際顯示四個層次;也以固定合成案例,檢查親切的文字是否暗示尚未成立的服務承諾。程式加入最小記憶體佇列與呼叫上限,先完成可核對的開發路徑,不把它包裝成正式營運系統。

Day 4 connects the previous Gemini experiment to a dedicated LINE test channel. It verifies the original webhook bytes before parsing, limits execution to an approved tester and two explicit commands, and keeps real chat data out of the model input. Webhook acknowledgement, model output, reply API acceptance, and visible delivery are treated as separate observations. A small in-memory worker keeps the request handler responsive, but does not provide durable delivery or production reliability. The chapter focuses on a verifiable interface boundary, not a completed service request.

https://ithelp.ithome.com.tw/upload/images/20260918/201206824kvdjOQUbx.png
圖 1:LOCAL Day 4 測試頻道的實際對話。左圖為 LOCAL ping 固定回覆,右圖為 LOCAL 測試觸發 Gemini 說明的真實手機顯示。對話中夾帶的「感謝您的訊息!很抱歉...」為官方帳號預設開啟的自動回應訊息,可於 LINE 後台關閉;本例已確認模型回覆與固定回覆皆成功經由 Webhook 與 Reply API 遞送至手機。畫面中的內容是合成案例說明,不是已建立或完成的服務案件。

看見的訊號 能確認什麼 不能直接推論什麼
Webhook 的 Verify 顯示 Success LINE 的驗證請求能到達入口並取得預期回應 Gemini 已執行、手機會收到文字
MODEL_TEXT_RECEIVED 取得符合本例基本條件的模型文字 語意正確、已完成任何業務操作
LINE_REPLY_ACCEPTED LINE 回覆 API 回應 HTTP 200 使用者已讀、現場服務已安排
手機顯示回覆 測試者在這次對話中看見訊息 回答一定正確、真人已接手

一、從終端機走到手機,不只是換一個輸出位置

前一篇的 Gemini 回答,已經能把「待核對」與「已完成」分開。但最後一句「請稍候專人確認」,仍可能讓使用者以為有人正準備處理;「原單號」也可能讓人以為手上已有一張可查詢的單。[1]

在終端機裡,作者知道這是合成實驗;放進 LINE,使用者看到的卻是一個服務窗口。同一句話換了位置,背後的期待也會改變。

因此,今天不只要讓手機出現 Gemini 的文字,還要能沿路回答:這則訊息從哪裡來?誰能觸發模型?送給模型的是什麼?最後的成功訊號究竟代表哪一步?

這次只做開發用的一對一測試。先傳 LOCAL ping,檢查 LINE 的固定回覆;再傳 LOCAL 測試,讓後端載入 Day 3 的同一份合成資料,請 Gemini 說明狀態。

這不是開放式客服,也不是把使用者輸入的任何文字都直接轉送 Google。固定指令看似保守,卻有一個優點:入口變了,問題與資料仍能控制,除錯時不必同時猜是哪個變因造成差異。

二、先驗證來源,不讓漂亮的 JSON 直接進入系統

LINE 的 Webhook 帶有 x-line-signature。核對時需要的是原始請求內容與該頻道的 Channel secret,而不是先解析、重新排版後的 JSON。[2]

核心邏輯很短:

expected = hmac.new(
    channel_secret.encode("utf-8"),
    raw_body,
    hashlib.sha256,
).digest()

supplied = base64.b64decode(signature, validate=True)
verified = hmac.compare_digest(expected, supplied)

這是重點節錄;完整程式另外檢查簽章長度、編碼與缺漏。不要直接把未處理例外的片段當成完整入口。

最容易忽略的是 raw_body。以下兩份 JSON 對人來說相同,位元組卻不同:

{"events":[]}
{ "events": [] }

本例保留第一份的簽章,只改空白再送一次,預期得到 401,而且模型與 LINE 回覆都不應被呼叫。這不是測試模型聰不聰明,而是檢查請求能不能越過入口。[2]

FastAPI 這裡直接讀取 Request 的原始內容,並在讀取過程限制大小;沒有先交給資料模型轉換。直接使用 Request,也代表後面的結構檢查要由應用程式處理。[3]

驗簽證明的是訊息來源與完整性,不是使用者已取得所有業務權限。 持有 Channel secret 的人也能產生簽章,所以金鑰必須保密;不能拿驗簽通過代替授權。

三、四個看起來像密碼的值,其實各有用途

設定 本篇用途 不應混用的地方
Channel secret 驗證 LINE Webhook 簽章 不是呼叫 Gemini 或傳送訊息的金鑰
Channel access token 呼叫 LINE 回覆 API 不是每個事件附帶的 reply token
replyToken 回覆這一次可回應的事件 不是可以長期保存、任意重用的通行證
GEMINI_API_KEY 使用 Gemini Developer API 不放進 LINE 訊息或模型提示詞

LINE 的回覆 API 使用該事件提供的 reply token;官方要求盡快使用,同一 token 只能使用一次,不能把它當成任意延後仍保證有效的資源。[4]

另外,本篇以 LINE_TEST_USER_ID 限制測試者。它是該 Provider 下的使用者識別,不是搜尋好友用的 LINE ID,也不是 @ 開頭的官方帳號識別。開發者可在頻道的 Basic settings 查看自己的 User ID。[5]

程式只接受允許帳號的一對一文字事件,且事件為 active。其他帳號、群組、圖片、一般聊天文字都略過。這份允許清單只解決「誰可以參與今天的實驗」,不假裝已完成正式會員或業務權限系統。

四、先用 ping 切開問題,再把 Gemini 接進來

LINE Developers Console 的 Verify 會送出沒有實際事件的請求,例如 events: [];簽章正確時,入口應回 HTTP 200。[6]

因此,Verify 成功之後,還需要兩個手機操作:

指令 路徑 除錯目的
LOCAL ping LINE → 本機入口 → 固定文字 → LINE 先排除模型因素,核對頻道與回覆路徑
LOCAL 測試 LINE → 本機入口 → 固定合成案例 → Gemini → LINE 檢查真正模型回合與手機顯示

如果 ping 都沒有回來,就先看 Webhook、使用者識別或 LINE 回覆結果,不必急著換 Gemini 模型。反過來,ping 正常而模型分支失敗,才把注意力集中在 SDK、金鑰、配額、模型或時間限制。

本例沿用 Day 3 的 gemini-3.8-flashLOWgoogle-genai==2.23.0。這些來自前篇的實驗基準,不是今天重新宣稱的最新版本或所有帳號都可用的保證。

程式直接讀取 examples/day03/run.py 的合成案例組裝與回覆擷取函式,不執行前篇的命令列入口,也不回改已發布程式。Google 呼叫仍走 client.models.generate_content(),沒有工具或自動函式執行。[7]

今天另外為 LINE 加上短句與語氣限制:不要使用 Markdown 標記,不要暗示已有真人接手,也不要把內部操作識別碼講成使用者已持有的單號。提示詞調整是新的實驗條件,不是已證明改善語氣的結果。

五、收件與回覆分開,但先把記憶體佇列的限制說清楚

LINE 官方建議非同步處理事件,避免當前工作卡住後續請求。[8] 因此,本例不在 Webhook 的 HTTP 處理流程裡等待 Gemini 回答,而是先把符合範圍的事件交給單一背景工作執行緒:

LINE Webhook → 原始內容驗簽 → 事件與測試範圍檢查
             → 記憶體佇列 → HTTP 回應
                            ↓
                    Gemini → LINE Reply API

這只是同一行程內的佇列。一個工作執行緒、一格待處理空間;佇列滿就回 503,不悄悄宣稱全部事件都已接好。

同一個 webhookEventId 在當次行程中只處理一次,避免重送時立刻再呼叫模型。但關掉程式再啟動,記憶體紀錄就不在了。收到 HTTP 200 的工作,也可能在還沒回覆手機之前因行程中止而消失。

這個缺口沒有被隱藏:它會成為後續可靠收件、持久化工作與雲端部署的問題。今天不把一個背景執行緒稱為可靠訊息系統,也不偷渡「精確一次」的保證。

六、模型逾時,怎麼回覆才不多出承諾?

本例把 Gemini HTTP 逾時設為 15 秒、LINE HTTP 逾時設為 8 秒,兩者都不自動重試;每次啟動最多三次模型嘗試與八次 LINE 回覆嘗試。這些是開發時的控制值,不是官方配額、硬性帳單上限或遠端一定停止運算的保證。

如果模型逾時、沒有文字、被截斷,或出現本例不接受的內容,程式準備的是固定降級說明:

【合成案例演練,非真實案件】這次暫時無法產生可用的說明;沒有查詢、送出或安排任何服務。

這是系統自己選定的文字,不能放在成果裡說成「Gemini 回答成功」。若成功取得模型文字,則保留原文,前面加上同樣的合成案例標示,不默默潤飾模型回答。

如果 LINE 回覆請求本身逾時,就記錄 LINE_REPLY_UNKNOWN,不盲目重送、不改成 push,也不重新呼叫 Gemini。不知道訊息是否已被接受,和確定沒有送出,是不同狀態。

程式另檢查工作在本機等待的時間,太晚就不再嘗試 reply。這只是減少使用過期 token 的機會,不能保證平台必然接受;若要可靠完成長任務,仍須重新設計結果查詢與通知方式。

七、在 Antigravity 檢查的是路徑與結果,不是尋找一個按鈕

這篇沿用既有 2026ironman 工作區。共用規則放根目錄,程式 Repo 位於 GitHub/local-service-agent-for-line,Day 4 的稿件與證據放在 LOCAL-Day04/editorial。不用再把新篇章放進 Day 2 的 imports。

Antigravity 可以協助盤點、提出 Implementation Plan、執行核准的本機工作,作者則檢查它是否只新增 Day 4、是否保留舊版本,以及輸出是否符合今天的範圍。[9]

在 Repo 根目錄,先用本日虛擬環境做離線驗證:

examples/day04/.venv/bin/python examples/day04/verify.py \
  --output ../../LOCAL-Day04/editorial/evidence

需要真實測試時,再由作者設定私人檔案並核准啟動:

examples/day04/.venv/bin/python examples/day04/app.py --live \
  --line-env ../../LOCAL-Day04/editorial/private/line.env \
  --gemini-env ../../LOCAL-Day03/editorial/private/.env \
  --output ../../LOCAL-Day04/editorial/evidence

這是本系列資料夾配置下的相對路徑,不是每個人都應照抄的固定位置。與 Day 3 不同,Day 4 明確讀取這兩份檔案,不優先取不明的同名環境變數。

LINE 需要能從外部存取的 HTTPS Webhook。可使用既有、獲准的開發入口;本機沒有入口時,可用臨時 HTTPS 通道,指向 127.0.0.1:8000,Webhook URL 加上 /webhook。[10]

Cloudflare Quick Tunnel 是其中一種開發方式,不需更動正式網站的 DNS,但它不是長期部署與可用性承諾。[11] 通道會將這個測試服務公開,必須先完成驗簽與測試者限制。只開放 Webhook 服務,不能拿檔案伺服器把整個工作目錄公開。

REPORT.html 留在本機,服務沒有 /docs、OpenAPI 或報告下載路由。即使知道臨時網址,也不應因此取得本機文章、金鑰與測試紀錄。

八、反例比單純看到回覆更重要

範例的離線測試涵蓋以下幾組問題,模型與 LINE HTTP 都使用明確標示的測試替身:

案例 必須觀察的結果
缺少/錯誤簽章、只多一個空白 拒絕處理,沒有模型與 LINE 回覆呼叫
正確簽章的空事件 回 200,但不產生訊息
非測試者、群組、圖片與任意文字 不進入模型路徑
正確指令 收件與背景工作分開,再檢查回覆種類
相同事件重送、佇列已滿 當次行程去重,或明確回應未能入列
模型逾時、截斷或非文字輸出 選固定降級文字,不假報模型成功
LINE 回覆逾時 保留未知,不自動重送
行程重建 展示記憶體去重已消失,不掩飾限制
模型文字含 HTML 特殊字元 報告以文字呈現,不把它當網頁程式執行

範例目前列出 50 項離線測試;案例數不等於作者本機已通過,更不等於五十次真實 LINE 或 Gemini 呼叫成功。執行後產生的 verification.json 才是當次離線結果;真實回合另外存於新的執行資料夾。

本次執行與作者判讀

項目 本次紀錄
本機離線測試 50 項,結果 PASS
Python/系統 3.13.5/Darwin
本次服務啟動時間(UTC) 2026-09-18T00:35:03.225784+00:00
指定模型/思考等級 gemini-3.8-flash/LOW
本次紀錄內模型嘗試數 1
本次紀錄內模型錯誤數 0
模型文字的 LINE 回覆 API HTTP 200;不代表已讀或業務完成
服務回傳模型 gemini-3.8-flash
模型呼叫本機耗時(秒) 1.948
手機顯示 作者另行確認本次模型回覆已顯示

本次取得的模型文字(原文,尚未加上系統的合成案例前綴):

您好,目前系統顯示您的無障礙需求已送出確認,但處理連線逾時,因此仍處於待驗證狀態。我們目前無法確認請求是否成功留存、是否有真人受理,也無法保證現場的協助安排。建議後續由主辦單位的後端系統以原操作紀錄重新核對處理狀況,在此之前本對話並未完成任何登記或送出動作喔!

作者判讀:

模型文字親切有禮,清楚說明連線逾時與處於待驗證狀態,明確告知無法保證現場協助,並提醒本對話未完成任何登記,嚴格守住了未知邊界;在 LINE 的即時對話情境下,既不給予虛假承諾,也維持了良好的使用者體驗。

這些資料只描述本次實驗。單次耗時不是效能基準,API 回覆與手機觀察也不是整套 Agent 的可靠性保證。

九、今天先做一個小入口,不提前變成所有人的客服

LINE 的真實事件在本機包含來源識別、事件資訊與 reply token;它們只供這次路由與回覆使用。送給 Gemini 的是既有合成案例,不含原始私人訊息或 LINE 身分資料。報告也不寫原始 Webhook 本文與秘密值。

但臨時通道、Google 與 LINE 仍是資料傳輸中的外部服務,不能因此宣稱完全沒有資料處理風險。測試只用本人與專用頻道,結束後關閉測試頻道的 Webhook、停止通道與本機服務,保存去識別證據。

本篇尚未處理正式身分綁定、持久化去重、撤回事件清除、多人服務、正式容量或長時間維運;也沒有 ADK 與業務工具。它留下的是一個可拆解、可測試的入口,而不是宣稱完成可直接商用的 AI 客服。

十、Day 5:入口有了,接著讓 Agent 查得到依據

前一篇是「模型收到資料後會怎麼說」,今天是「訊息如何安全進出 LINE」。接下來才有條件引入 ADK 與第一個受控查詢工具,讓 LOCAL 不只是解釋給定資料,而是依需求查回可核對的服務資訊。

這條順序很重要:入口驗簽不能代替工具授權,訊息傳送成功也不能代替業務完成。 每長出一項能力,就要知道新增了哪一種證據,還留下哪個問題。

手機上的一句話,背後可能經過好幾個「成功」。今天把它們拆開,不是讓服務變得難用,而是讓未來那句「已經幫你處理好了」有資格被說出口。

程式與參考資料

LOCAL 專案:本篇程式位於 examples/day04/,重現入口位於 docs/day04/。搭配當次程式版本與執行紀錄閱讀。

前篇:Day 3|從 AI Studio 到 Gemini API:建立 LOCAL 的第一個可重現模型實驗


上一篇
Day 3|從 AI Studio 到 Gemini API:建立 LOCAL 的第一個可重現模型實驗
系列文
LOCAL:30 天打造 LINE × Google AI 地方服務 Agent4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言